iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
JavaScript

QA 的 JavaScript 初心之路:30 天做出一個能玩的戰鬥小遊戲《勇者 vs 小怪》系列 第 7

Day07|迴圈:我的主控台跑了 106082 次還停不下來

  • 分享至 

  • xImage
  •  

今天的主題:迴圈——重複做事的語法,以及我親手讓瀏覽器當機的那一次

寫遊戲一定會遇到「重複做的事」:五隻怪輪流報名、回合制要一格一格跑、清單要一筆一筆印。總不能複製貼上五十行 console.log——所以今天學迴圈:把同一段程式交給電腦跑好幾次。

for 迴圈:三段式

const enemies = ["雜魚", "小怪", "菁英怪", "小BOSS", "魔王"];

for (let i = 0; i < enemies.length; i++) {
  console.log(i, "→", enemies[i]);
}

for 後面括號裡有三段,用分號隔開,執行順序是:

  1. let i = 0 — 起點,整個迴圈只執行這一次
  2. i < enemies.length每一輪開始前先檢查,通過才跑內容
  3. i++每一輪跑完之後才執行

輸出:

0 → 雜魚
1 → 小怪
2 → 菁英怪
3 → 小BOSS
4 → 魔王

起點是 0、條件用「嚴格小於 <」、終點用 length——這三個湊在一起,i 剛好掃過 0 到 4,也就是這個陣列的每一個合法索引,一個不多、一個不少。這不是巧合,是語言設計故意配好的。還記得 Day 5 說的「length 本身剛好是第一個不存在的索引」嗎?今天這行程式就是那個知識的實際應用。

while 迴圈:更自由,也更危險

while 只保留三段裡的「檢查」,起點和 i++ 都要自己管:

let i = 0;
while (i < 5) {
  console.log(i);
  i++;
}

輸出是 0、1、2、3、4,跟 for 版本一樣。

插入 day7-2.png:while 版本的程式碼與輸出 0~4

但「自己管」就是危險的來源。下一節是我的親身經歷。

我讓瀏覽器當機了

上面的 while 我一開始忘記寫 i++:

let i = 0;
while (i < 5) {
  console.log(i);
  // i++; ← 忘了這行
}

i 永遠是 0,0 < 5 永遠成立,迴圈永遠不會停。按下執行後的現象依序是:

  1. 主控台出現一個 0,旁邊有一個數字在飛快跳動——那是 Chrome 的重複計數器:同一則訊息連續出現時,Chrome 不會真的畫出幾十萬行,而是折疊成一行,用數字顯示「這則訊息重複了幾次」
  2. 電腦像是飛機起飛一樣發出噪音(風扇開始全速運轉,CPU 被這個停不下來的迴圈吃到滿)
  3. 網頁完全沒有反應,點什麼都不理我,整個電腦都卡卡的,感覺下一秒會出現Windows XP的無限彈跳視窗
  4. Chrome 跳出「網頁沒有回應」的警告——這是瀏覽器的最後防線,偵測到分頁卡死,可以強制終止它
  5. 我最後直接把整個瀏覽器關掉

插入 day7.png:當機當下的主控台,重複計數器停在 106082

關掉之前,計數器停在 106082——代表那一行 console.log(i) 在幾秒之內被執行了超過十萬次,而 i 從頭到尾都是 0。如果 Chrome 沒有折疊重複訊息,我的主控台會真的被畫出十萬行。

這次當機讓我體會到兩件事:

  • JS 在瀏覽器裡是單執行緒:一個迴圈不結束,同一個分頁的任何事都不能做,所以整個網頁才會跟著卡死
  • while 的自由是有代價的:for 的三段式至少把起點、條件、i++ 寫在同一行,一眼能檢查;while 把它們拆開,忘一步就是這種下場。寫 while 時我會多檢查一次「迴圈裡有沒有東西在往結束的方向前進」

意外發現:hp < 30i < 5 其實是同一條規則

修好迴圈之後,我發現一件事:Day 4 學的 hp < 30,當 hp 剛好是 30 時會被判成 false,走 else;今天的 i < 5,當 i 剛好是 5 時也是 false,迴圈結束——兩個其實是同一條規則:「剛好等於界線的值,不算通過」

但一開始我預期的結束數字不是 5,是 6。追了一下才知道自己漏想了一步:i++ 只有在「這一輪有成功執行」之後才會跑。最後一次成功的輪次是 i = 4,然後 i++ 把它變成 5,接著檢查 5 < 5 失敗——迴圈直接結束,檢查失敗之後就沒有機會再 i++。所以 i 的最終值是 5,永遠不會變成 6。

我在 while 裡每次 i++ 之後多印一次,親眼驗證了這件事:

let i = 0;
while (i < 5) {
  console.log(i, "→", enemies[i]);
  i++;
  console.log(i);
}

輸出的結尾是:

4 → 魔王
5        ← i 的「遺言」:它曾經變成 5,但 5 那一輪從來沒有執行

順手在主控台直接驗證了那個比較式:

console.log(5 < 5);   // false

再追下去:for 和 while 停下來的數字不一樣?

對照兩個迴圈的輸出時,我又冒出一個疑問:for 的最後一次印出是 4,while 的最後一次印出是 5——是兩個迴圈停下來的位置不同嗎?

追查後發現又是「印的位置」搞的鬼:for 的 console.log(i)i++ 之前,while 的第二個 console.log(i)i++ 之後。兩個迴圈的 i 最後其實都是 5,只是 for 的 5 沒有機會被印出來。

想證明這件事,我把 for 的計數器改成 j,並把宣告搬到迴圈外面,想直接在迴圈結束後印出它的值。沒想到連踩了兩個坑:

第一次,我照原本的寫法把 i 改成 j(let j = 0 還是寫在 for 括號裡),結果主控台跳出錯誤:ReferenceError: j is not defined。原來寫在 for 括號裡的 let j 只活在迴圈裡面,迴圈一結束它就消失了,外面的 console.log 根本找不到 j——這個錯誤反而直接證明了「迴圈裡宣告的變數,迴圈外問不到」。

第二次,我在上面加了一行 let j; 來宣告(本來想寫 let j = 0,手滑把 = 0 刪掉),錯誤確實消失了,但印出來的是 undefined,不是數字!
再追查才發現:加了 let j; 之後變成外面一個 j、迴圈裡又一個 j,兩個名字相同但完全不同——迴圈裡所有跑 0 到 5 的動作都是裡面那個 j 在做,外面那個從頭到尾沒被賦值過。
宣告了卻沒被放進任何東西的變數,值就是 undefined。這種現象叫遮蔽(shadowing):內層的同名變數暫時擋住了外層的,叫「j」的指令都先找內層那位。

而且這次的教訓很特別:用「加宣告」消掉錯誤,不代表修對了。大聲的 ReferenceError 消失了,換來的是安靜的 undefined——不報錯的錯,往往更難查。

最後把 for 括號裡的 let 拿掉,讓迴圈直接用外面那個 j,才終於印出:

for 結束後的 j: 5

兩個迴圈的內部結局一模一樣:計數器都走到 5 停下,只是 for 的 5 死得無聲無息。

插入 day7-4.png:for(用外面的 j)+ while 的輸出,含「for 結束後的 j: 5」和 while 結尾的 5

途中我還發現一個偷吃步:如果連外面的 let j; 都不寫,直接 for (j = 0; ...) 居然也跑得動,一樣印出 5。
查了才知道,對一個從來沒被宣告過的變數賦值,JS 不會報錯,而是默默自動生出一個全域變數——這叫隱式全域(implicit global)
雖然能用,但這是不推薦的寫法:變數會悄悄掛到整個網頁上,誰都碰得到,而且 JS 後來新增的「嚴格模式」底下這種寫法會直接報錯——它是 1995 年誕生時的歷史包袱,現代標準一直在勸退它。
乖乖宣告還是比較安全。

回頭看,這也解釋了為什麼「起點 0 + 嚴格小於 + length」這個組合不會出界:如果 i < 5 真的會跑到 6,那 enemies[6] 就是 undefined,這種 bug 會到處都是——幸好它不會。

小結

今天搞懂的事:

  • for 三段式:起點只跑一次、條件每輪開始前檢查、i++ 每輪結束後執行
  • 起點 0 + 嚴格小於 < + length,剛好掃過每個合法索引,這是刻意設計的組合
  • while 更自由但要自己管起點和前進,忘記 i++ 就是無窮迴圈
  • 無窮迴圈的實際景象:重複計數器十萬次、風扇全速、網頁卡死、瀏覽器跳出保護性警告
  • < 的規則全語言一致:剛好等於界線的值不算通過;i 的最終值會停在界線上(5),但那一輪從未執行
  • 迴圈括號裡 let 宣告的變數只活在迴圈裡,外面問不到(大聲的 ReferenceError);宣告同名變數會互相遮蔽,只會留下安靜的 undefined——不報錯的錯更難查

寫 QA 測試時我常常要「跑遍每一筆測試資料」,原來工程師是用迴圈做這件事的——明天學函式,把重複的邏輯包成一件工具,不用每次重新寫。


上一篇
Day06|物件登場:const 鎖不住的內容,鎖不住的陣列
系列文
QA 的 JavaScript 初心之路:30 天做出一個能玩的戰鬥小遊戲《勇者 vs 小怪》7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言